package cn.ilikes.tools.hbase.jpa.configs.tools;

import java.io.File;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.persistence.PersistenceException;
import javax.persistence.spi.PersistenceUnitTransactionType;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import cn.ilikes.tools.hbase.jpa.SysConfigs;
import cn.ilikes.tools.hbase.jpa.configs.JPAHBaseConfigurationException;
import cn.ilikes.tools.hbase.jpa.metadatas.JPAHBasePersistenceUnitMetadata;
import cn.ilikes.tools.hbase.jpa.metadatas.sub.configs.Datastorefactory;
import cn.ilikes.tools.hbase.jpa.metadatas.sub.configs.Datastorefactory.DataStore;
import cn.ilikes.tools.hbase.jpa.metadatas.sub.configs.Indexfactory;
import cn.ilikes.tools.hbase.jpa.metadatas.sub.configs.IndexConfigs;
import cn.ilikes.tools.logs.Loggerfactory;

public class PersistenceXMLLoader {

	private static Logger logger = Loggerfactory.instance(PersistenceXMLLoader.class);

	private String persistenceUnitsName;

	public PersistenceXMLLoader(String persistenceUnitsName) {
		this.persistenceUnitsName = persistenceUnitsName;
	}

	private String getPersistenceUnitsName() {
		return persistenceUnitsName;
	}

	private Document getDocument(URL configURL) throws JPAHBaseConfigurationPaserException {
		InputStream is = null;
		Document doc;
		try {
			is = null;
			if (configURL != null) {
				URLConnection conn = configURL.openConnection();
				conn.setUseCaches(false); // avoid JAR locking on Windows and Tomcat
				is = conn.getInputStream();
			}
			if (is == null) {
				throw new IOException("Failed to obtain InputStream from url: " + configURL);
			}

			DocumentBuilderFactory docBuilderFactory = null;
			docBuilderFactory = DocumentBuilderFactory.newInstance();
			docBuilderFactory.setNamespaceAware(true);

			final Schema v2Schema = SchemaFactory.newInstance(javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI)
					.newSchema(new StreamSource(getStreamFromClasspath("persistence_2_0.xsd")));
			final Validator v2Validator = v2Schema.newValidator();
			final Schema v1Schema = SchemaFactory.newInstance(javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI)
					.newSchema(new StreamSource(getStreamFromClasspath("persistence_1_0.xsd")));
			final Validator v1Validator = v1Schema.newValidator();

			InputSource source = new InputSource(is);
			DocumentBuilder docBuilder = null;
			try {
				docBuilder = docBuilderFactory.newDocumentBuilder();
			} catch (ParserConfigurationException e) {
				Loggerfactory.error(logger, "Error during parsing, Caused by: " + e.getMessage());
				throw new JPAHBaseConfigurationPaserException(e);
			}

			@SuppressWarnings("rawtypes")
			List errors = new ArrayList();
			docBuilder.setErrorHandler(new ErrorLogger("XML InputStream", errors));
			doc = docBuilder.parse(source);

			if (errors.size() == 0) {
				v2Validator.setErrorHandler(new ErrorLogger("XML InputStream", errors));
				v2Validator.validate(new DOMSource(doc));
				boolean isV1Schema = false;
				if (errors.size() != 0) {
					// v2 fails, it could be because the file is v1.
					Exception exception = (Exception) errors.get(0);
					final String errorMessage = exception.getMessage();
					// is it a validation error due to a v1 schema validated by
					// a v2
					isV1Schema = errorMessage.contains("1.0") && errorMessage.contains("2.0") && errorMessage.contains("version");
				}
				if (isV1Schema) {
					errors.clear();
					v1Validator.setErrorHandler(new ErrorLogger("XML InputStream", errors));
					v1Validator.validate(new DOMSource(doc));
				}
			} else {
				throw new JPAHBaseConfigurationPaserException("invalid persistence.xml", (Throwable) errors.get(0));
			}
		} catch (IOException e) {
			throw new JPAHBaseConfigurationPaserException(e);
		} catch (SAXException e) {
			throw new JPAHBaseConfigurationPaserException(e);
		} finally {
			try {
				is.close();
			} catch (IOException e) {
				throw new JPAHBaseConfigurationPaserException(e);
			}
		}

		return doc;
	}

	/**
	 * Get stream from classpath.
	 * 
	 * @param fileName
	 *                  the file name
	 * @return the stream
	 * @throws Exception
	 *                   the exception
	 */
	private InputStream getStreamFromClasspath(String fileName) {
		String path = fileName;
		InputStream dtdStream = PersistenceXMLLoader.class.getClassLoader().getResourceAsStream(path);
		return dtdStream;
	}

	//	private  List<JPAHBasePersistenceUnitMetadata> findPersistenceUnits(URL url) throws Exception {
	//		return findPersistenceUnits(url, PersistenceUnitTransactionType.RESOURCE_LOCAL);
	//	}

	/**
	 * 
	 * @param persistencefile
	 *                  persistence.xml file
	 * @return K : PersistenceUnitName
	 * @throws JPAHBaseConfigurationPaserException
	 */
	public Map<String, JPAHBasePersistenceUnitMetadata> paserPersistenceMetadatas(String persistencefile) throws JPAHBaseConfigurationPaserException {
		Loggerfactory.info(logger, "start analysing persistence.xml: " + persistencefile);
		Map<String, JPAHBasePersistenceUnitMetadata> persistenceUnitMap = new HashMap<String, JPAHBasePersistenceUnitMetadata>();
		Enumeration<URL> xmls = null;
		try {
			xmls = Thread.currentThread().getContextClassLoader().getResources(persistencefile);
			if (xmls == null || !xmls.hasMoreElements()) {
				Loggerfactory.error(logger, "Could not find persistence  file " + persistencefile);
				throw new JPAHBaseConfigurationPaserException("Could not find any file : " + persistencefile);
			}
		} catch (IOException ioex) {
			throw new JPAHBaseConfigurationPaserException("Could not find any file : " + persistencefile, ioex);
		}

		while (xmls.hasMoreElements()) {
			URL url = xmls.nextElement();
			List<JPAHBasePersistenceUnitMetadata> metadataFiles = findPersistenceUnits(url, PersistenceUnitTransactionType.RESOURCE_LOCAL);
			for (JPAHBasePersistenceUnitMetadata metadata : metadataFiles) {
				// check for unique names
				String persistenceUnitName = metadata.getPersistenceUnitName();
				if (persistenceUnitMap.keySet().contains(metadata.getPersistenceUnitName())) {
					throw new JPAHBaseConfigurationPaserException("Duplicate persistence-units for name: " + persistenceUnitName + ". verify your persistence.xml file");
				}

				// check for provider
				if (metadata.getPersistenceProviderClassName() != null && SysConfigs.PROVIDER_IMPLEMENTATION_NAME.equalsIgnoreCase(metadata.getPersistenceProviderClassName())) {
					{
						//paser index manager factory 
						String indexConfig = (String) metadata.getProperties().get(SysConfigs.jpa_index_config_file);

						Loggerfactory.info(logger, "start paser index factory ; and find index factory name : " + persistenceUnitName + "'s index configs info in "
								+ indexConfig);

						if (!indexConfig.isEmpty()) {
							Loggerfactory.info(logger, "persistence unit metadata factory  load  index config   ; config file    : " + indexConfig);
							IndexConfigs indexfactorys = PaserSubConfigs.parserIndexConfigs(indexConfig);
							if (indexfactorys == null) {
								Loggerfactory.error(logger, "persistence unit  paser index factory  config error    : " + indexConfig,
										new JPAHBaseConfigurationException("persistence unit  paser index factory  config error    : "
												+ indexConfig));
							} else {
								Indexfactory indexfactory = indexfactorys.findFactoryByName(persistenceUnitName);

								if (indexfactory == null)
									throw new JPAHBaseConfigurationPaserException("not find persistenceUnitName : " + persistenceUnitName
											+ " index manager config ；checking index config file contain " + persistenceUnitName
											+ " index config ; index config name == persistenceUnitName  " + persistenceUnitName);
								else {
									Loggerfactory.info(logger, "success  paser  persistenceUnitName : " + persistenceUnitName
											+ "'s index configs info ; " + indexConfig);
									metadata.setIndexfactory(indexfactory);
								}
							}
						} else {
							throw new JPAHBaseConfigurationPaserException("nit find persistenceUnitName : persistenceUnitName" + persistenceUnitName
									+ " index manager config file " + indexConfig);
						}
					}

					{
						//paser datasore factory
						String config = (String) metadata.getProperties().get(SysConfigs.jpa_datastore_config_file);
						if (!config.isEmpty()) {
							Loggerfactory.info(logger, "persistence unit metadata factory  load  datastoe  config   ; config file    : " + config);
							Datastorefactory datastorefacpory = PaserSubConfigs.parsedataStoreFactory(config);
							if (datastorefacpory == null) {
								Loggerfactory.error(logger, "persistence unit  paser index factory  config error    : " + config,
										new JPAHBaseConfigurationException("persistence unit  paser index factory  config error    : "
												+ config));
							} else {
								DataStore dataStore = datastorefacpory.findDataStore(persistenceUnitName);
								if (dataStore == null)
									throw new JPAHBaseConfigurationPaserException("not find persistenceUnitName : " + persistenceUnitName
											+ " dataStore  config ；checking dataStore config file contain " + persistenceUnitName
											+ " dataStore config ; dataStore config name == persistenceUnitName  "
											+ persistenceUnitName);
								else {
									Loggerfactory.info(logger, "success  paser  persistenceUnitName : " + persistenceUnitName
											+ "'s dataStore configs info ; " + dataStore);
									metadata.setDataStore(dataStore);
								}

							}
						}
					}
					if (Loggerfactory.isdebug(logger))
						Loggerfactory.debug(logger, "load   Persistence Unit Name " + persistencefile + " JPAHBasePersistenceUnitMetadata   " + metadata);
					persistenceUnitMap.put(metadata.getPersistenceUnitName(), metadata);
				} else {
					throw new JPAHBaseConfigurationPaserException("Persistence Provider Class is " + metadata.getPersistenceProviderClassName() + " or it is not "
							+ SysConfigs.PROVIDER_IMPLEMENTATION_NAME);
				}

			}
		}

		return persistenceUnitMap;
	}

	private List<JPAHBasePersistenceUnitMetadata> findPersistenceUnits(final URL url, PersistenceUnitTransactionType defaultTransactionType) throws JPAHBaseConfigurationPaserException {

		Document doc = getDocument(url);

		doc.getXmlVersion();
		Element top = doc.getDocumentElement();

		String versionName = top.getAttribute("version");

		NodeList children = top.getChildNodes();
		ArrayList<JPAHBasePersistenceUnitMetadata> units = new ArrayList<JPAHBasePersistenceUnitMetadata>();
		for (int i = 0; i < children.getLength(); i++) {
			if (children.item(i).getNodeType() == Node.ELEMENT_NODE) {
				Element element = (Element) children.item(i);
				String tag = element.getTagName();
				// look for "persistence-unit" element
				if (tag.equals("persistence-unit")) {
					//paser persistence-unit
					JPAHBasePersistenceUnitMetadata metadata = parsePersistenceUnit(url, element, versionName);
					if (metadata != null)
						units.add(metadata);
				}
			}
		}
		return units;
	}

	private JPAHBasePersistenceUnitMetadata parsePersistenceUnit(final URL url, Element top, final String versionName) throws JPAHBaseConfigurationPaserException {
		JPAHBasePersistenceUnitMetadata metadata = new JPAHBasePersistenceUnitMetadata(versionName, getPersistenceRootUrl(url));

		String puName = top.getAttribute("name");
		if (!isEmpty(puName)) {
			if (!getPersistenceUnitsName().equals(puName))
				return null;
			Loggerfactory.info(logger, "parser  persistence unit  name  from  persistence.xml  ;  persistence unit  name  : " + puName);
			metadata.setPersistenceUnitName(puName);
			String transactionType = top.getAttribute("transaction-type");
			if (StringUtils.isEmpty(transactionType) || PersistenceUnitTransactionType.RESOURCE_LOCAL.name().equals(transactionType)) {
				metadata.setTransactionType(PersistenceUnitTransactionType.RESOURCE_LOCAL);
			} else if (PersistenceUnitTransactionType.JTA.name().equals(transactionType)) {
				metadata.setTransactionType(PersistenceUnitTransactionType.JTA);
			}
		} else {
			Loggerfactory.error(logger, "parser  persistence unit  name from  persistence.xml , persistence unit  name is null  " + puName);
			throw new JPAHBaseConfigurationPaserException("parser  persistence unit  name from  persistence.xml , persistence unit  name is null  " + puName);
		}

		NodeList children = top.getChildNodes();
		for (int i = 0; i < children.getLength(); i++) {
			if (children.item(i).getNodeType() == Node.ELEMENT_NODE) {
				Element element = (Element) children.item(i);
				String tag = element.getTagName();
				if (tag.equals("mapping-file")) {
					metadata.getMapfile().add(getElementContent(element));
				}
				if (tag.equals("provider")) {
					metadata.setProvider(getElementContent(element));
				} else if (tag.equals("properties")) {
					NodeList props = element.getChildNodes();
					for (int j = 0; j < props.getLength(); j++) {
						if (props.item(j).getNodeType() == Node.ELEMENT_NODE) {
							Element propElement = (Element) props.item(j);
							// if element is not "property" then skip
							if (!"property".equals(propElement.getTagName())) {
								continue;
							}

							String propName = propElement.getAttribute("name").trim();
							String propValue = propElement.getAttribute("value").trim();
							if (isEmpty(propValue)) {
								throw new JPAHBaseConfigurationPaserException("properties name : " + propName + " value is  empty ! ");
								//propValue = getElementContent(propElement, "");
							}
							metadata.getProperties().put(propName, propValue);
						}
					}
				} else if (tag.equals("class")) {
					String clazz = getElementContent(element).trim();
					if (clazz == null || clazz.isEmpty()) {
						Loggerfactory.warn(logger, "persistence  unit " + puName + "  <class> contain  empty  content is empty");
					} else {
						Loggerfactory.info(logger, "persistence  unit " + puName + "  <class> contain  empty  :  " + clazz);
						metadata.getClasses().add(clazz);
					}

				} else if (tag.equals("jar-file")) {
					String jarfile = getElementContent(element);
					if (jarfile.isEmpty())
						throw new JPAHBaseConfigurationPaserException("jar-file   have   empty   value  ! ");
					metadata.getJarFiles().add(getElementContent(element));
				} else if (tag.equals("exclude-unlisted-classes")) {
					metadata.setExcludeUnlistedClasses(true);
				}
			}
		}
		PersistenceUnitTransactionType transactionType = getTransactionType(top.getAttribute("transaction-type"));
		if (transactionType != null) {
			metadata.setTransactionType(transactionType);
		}

		if (Loggerfactory.isdebug(logger))
			Loggerfactory.debug(logger, "find PersistenceUnitName : " + metadata.getPersistenceUnitName() + "   ; metadata : " + metadata);
		return metadata;
	}

	private PersistenceUnitTransactionType getTransactionType(String elementContent) {

		if (elementContent == null || elementContent.isEmpty()) {
			return null;
		} else if (elementContent.equalsIgnoreCase("JTA")) {
			return PersistenceUnitTransactionType.JTA;
		} else if (elementContent.equalsIgnoreCase("RESOURCE_LOCAL")) {
			return PersistenceUnitTransactionType.RESOURCE_LOCAL;
		} else {
			throw new PersistenceException("Unknown TransactionType: " + elementContent);
		}
	}

	/**
	 * The Class ErrorLogger.
	 */
	public class ErrorLogger implements ErrorHandler {

		/** The file. */
		private String file;

		/** The errors. */
		private List errors;

		/**
		 * Instantiates a new error logger.
		 * 
		 * @param file
		 *                  the file
		 * @param errors
		 *                  the errors
		 */
		ErrorLogger(String file, List errors) {
			this.file = file;
			this.errors = errors;
		}

		public void error(SAXParseException error) {
			Loggerfactory.error(logger, "Error parsing XML: " + file + '(' + error.getLineNumber() + ") " + error.getMessage());
			errors.add(error);
		}

		public void fatalError(SAXParseException error) {
			Loggerfactory.error(logger, "Error parsing XML: " + file + '(' + error.getLineNumber() + ") " + error.getMessage());
			errors.add(error);
		}

		public void warning(SAXParseException warn) {
			Loggerfactory.warn(logger, "Warning parsing XML: " + file + '(' + warn.getLineNumber() + ") " + warn.getMessage());
		}
	}

	private boolean isEmpty(String str) {
		return null == str || str.isEmpty();
	}

	private String getElementContent(final Element element) {
		return getElementContent(element, null);
	}

	private String getElementContent(Element element, String defaultStr) {
		if (element == null) {
			return defaultStr;
		}

		NodeList children = element.getChildNodes();
		StringBuilder result = new StringBuilder("");
		for (int i = 0; i < children.getLength(); i++) {
			if (children.item(i).getNodeType() == Node.TEXT_NODE || children.item(i).getNodeType() == Node.CDATA_SECTION_NODE) {
				result.append(children.item(i).getNodeValue());
			}
		}
		return result.toString().trim();
	}

	private URL getPersistenceRootUrl(URL url) {

		String f = url.getFile();
		f = parseFilePath(f);
		URL jarUrl = url;
		try {
			if (AllowedProtocol.isJarProtocol(url.getProtocol())) {
				jarUrl = new URL(f);
				if (jarUrl.getProtocol() != null && AllowedProtocol.FILE.name().equals(jarUrl.getProtocol().toUpperCase()) && StringUtils.contains(f, " ")) {
					jarUrl = new File(f).toURI().toURL();
				}
			} else if (AllowedProtocol.isValidProtocol(url.getProtocol())) {
				if (StringUtils.contains(f, " ")) {
					jarUrl = new File(f).toURI().toURL();
				} else {
					jarUrl = new File(f).toURL();
				}
			}
		} catch (MalformedURLException mex) {
			Loggerfactory.error(logger, "Error during getPersistenceRootUrl(), Caused by: " + mex.getMessage());
			throw new IllegalArgumentException("invalid jar URL[] provided!" + url);
		}

		return jarUrl;
	}

	private String parseFilePath(String file) {
		final String excludePattern = "/META-INF/persistence.xml";
		file = file.substring(0, file.length() - excludePattern.length());

		// in case protocol is "file".
		file = file.endsWith("!") ? file.substring(0, file.length() - 1) : file;

		return file;
	}

	public enum AllowedProtocol {
		WSJAR, JAR, ZIP, FILE, VFSZIP;

		/**
		 * In case it is jar protocol
		 * 
		 * @param protocol
		 * @return
		 */
		public static boolean isJarProtocol(String protocol) {
			return protocol != null && (protocol.toUpperCase().equals(JAR.name()) || protocol.toUpperCase().equals(WSJAR.name()));
		}

		/**
		 * If provided protocol is within allowed protocol.
		 * 
		 * @param protocol
		 *                  protocol
		 * @return true, if it is in allowed protocol.
		 */
		public static boolean isValidProtocol(String protocol) {
			try {
				AllowedProtocol.valueOf(protocol.toUpperCase());
				return true;
			} catch (IllegalArgumentException iex) {
				return false;
			}
		}
	}

}
